package zheezes.mail;

import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.UnknownHostException;
import java.nio.ByteBuffer;
import java.nio.channels.ClosedChannelException;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.Iterator;

import zheezes.util.ZBuffer;

public class ZSmtp {
	public final static int SMTP_PORT = 25;

	private final static int RCVBUF_SIZE = 4096;
	private final static int SNDBUF_SIZE = 4096;

	public final static int ERR_MAIL = -100;
	public final static int ERR_MX = -101;
	public final static int ERR_CONN = -102;
	public final static int ERR_SND = -103;
	public final static int ERR_RCV = -104;
	
	public final static int CODE_READY = 220;
	public final static int CODE_OK = 250;
	public final static int CODE_UNAVAILABLE = 550;

	public final static int CMD_HELO = 2;
	public final static int CMD_MAIL_FROM = 3;
	public final static int CMD_RCPT_TO = 4;
	public final static int CMD_QUIT = 5;
	
	private Selector selector = null;
	private SocketChannel selfd = null;
	private SocketChannel sock = null;

	private ByteBuffer rcvbuf = ByteBuffer.allocate(RCVBUF_SIZE);
	private ByteBuffer sndbuf = ByteBuffer.allocate(SNDBUF_SIZE);

	private int timeout = 0;

	private int cmd = 0;
	private int code = 0;
	
	private int error = 0;

	private boolean available = false;

	public int getTimeout() {
		return timeout;
	}

	public void setTimeout(int timeout) {
		this.timeout = timeout;
	}

	public void setRcvBufSize(int size) {
		ByteBuffer old = rcvbuf;
		rcvbuf = ByteBuffer.allocate(size);
		rcvbuf.put(old.array());
	}

	public int getRcvBufSize() {
		return rcvbuf.capacity();
	}

	public int getCmd() {
		return cmd;
	}

	public int getCode() {
		return code;
	}

	public int getError() {
		return error;
	}
	
	public static int getCode(String resp) {
		int idx = resp.indexOf(0x20);
		String str = resp.substring(0, idx);
		int code = -1;
		try {
			code = Integer.parseInt(str);
		} catch (NumberFormatException e) {
			e.printStackTrace();
		}
		return code;
	}

	public static int getCode(byte[] resp) {
		if (resp == null) {
			return -1;
		}
		return getCode(new String(resp));
	}

	public static String makeSmtpRequest(int cmd, String data) {
		String ret = null;
		switch (cmd) {
		case CMD_HELO:
			ret = String.format("HELO %s\r\n", data);
			break;
		case CMD_MAIL_FROM:
			ret = String.format("MAIL FROM: <%s>\r\n", data);
			break;
		case CMD_RCPT_TO:
			ret = String.format("RCPT TO: <%s>\r\n", data);
			break;
		case CMD_QUIT:
			ret = String.format("QUIT\r\n");
			break;
		}
		return ret;
	}

	private Selector getSelector() {
		if (selector == null) {
			try {
				selector = Selector.open();
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
		return selector;
	}

	public int reset() {
		sndbuf.clear();
		rcvbuf.clear();
		cmd = 0;
		code = 0;
		return 0;
	}

	/**
	 *  dom, domain or mail
	 */
	public int connect(String dom, int port) {
		String domain = ZDns.getTopDomain(dom);
		String mxs = ZDns.getOneMxHost(domain, -1);
		InetAddress addr = null;
		try {
			addr = InetAddress.getByName(mxs);
		} catch (UnknownHostException e) {
		}
		if (domain == null || mxs == null || addr == null) {
			error = ERR_MX;
			return -1;
		}
		return connect(addr, port);
	}

	public int connect(InetAddress host, int port) {
		// can connect only once
		if (selfd != null || selector != null || sock != null) {
			return -1;
		}

		int ret = -1;

		try {
			selfd = SocketChannel.open();
			selfd.configureBlocking(false);
			selector = getSelector(); // Selector.open();
			if (selector == null) {
				return -1;
			}
			selfd.register(selector, SelectionKey.OP_CONNECT);
			selfd.connect(new InetSocketAddress(host, port));
			int num = selector.select(timeout);
			if (num <= 0) {
				error = ERR_CONN;
				return -1;
			}
			Iterator<SelectionKey> iter = selector.selectedKeys().iterator();
			while (iter.hasNext()) {
				SelectionKey sk = iter.next();
				iter.remove();

				sock = (SocketChannel) sk.channel();
				if (sk.isConnectable()) {
					if (sock.isConnectionPending()) {
						sock.finishConnect();
					}
					sock.register(selector, SelectionKey.OP_READ);
					available = true;
					ret = 0;
				}
			}
		} catch (ClosedChannelException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		}

		return ret;
	}

	public int close() {
		Iterator<SelectionKey> iter = selector.selectedKeys().iterator();
		while (iter.hasNext()) {
			iter.remove();
		}
		try {
			sock.close();
			selector.close();
			selfd.close();
		} catch (IOException e) {
			e.printStackTrace();
		}
		sock = null;
		selector = null;
		selfd = null;
		return 0;
	}

	public boolean available() {
		return available;
	}

	public int sendRequest(int cmd, String data) {
		String request = makeSmtpRequest(cmd, data);
		sndbuf.clear(); // for write
		sndbuf.put(request.getBytes());
		sndbuf.flip(); // for read
		long count = -1;
		try {
			count = sock.write(sndbuf);
		} catch (IOException e) {
			e.printStackTrace();
		}
		if (count < 0) {
			error = ERR_SND;
			available = false;
			return -1;
		}
		this.cmd = cmd;
		this.code = 0;

		return 0;
	}

	public byte[] recvResponse() {
		int count = -1;
		try {
			count = sock.read(rcvbuf);
		} catch (IOException e) {
			e.printStackTrace();
		}
		if (count < 0) {
			error = ERR_RCV;
			available = false;
			return null;
		}
		rcvbuf.flip(); // for read
		byte[] resp = ZBuffer.readLastLine(rcvbuf);
		rcvbuf.compact(); // for write

		return resp;
	}

	public int waitFor(int timeout, int event) {
		int num = -1;
		try {
			num = selector.select(timeout);
		} catch (IOException e) {
			e.printStackTrace();
		}
		if (num <= 0) {
			return -1;
		}
		int count = 0;
		Iterator<SelectionKey> iter = selector.selectedKeys().iterator();
		while (iter.hasNext()) {
			SelectionKey sk = iter.next();
			if ((event & SelectionKey.OP_READ) != 0 && (sk.isReadable())) {
				count++;
			} else if ((event & SelectionKey.OP_WRITE) != 0
					&& (sk.isWritable())) {
				count++;
			}
			iter.remove();
		}
		return count;
	}

	public int waitForRead(int timeout) {
		return waitFor(timeout, SelectionKey.OP_READ);
	}

	public int waitForRead() {
		return waitForRead(timeout);
	}

	public byte[] ask(int cmd, String data) {
		long begin = System.currentTimeMillis();
		byte[] resp = null;
		if (sendRequest(cmd, data) < 0) {
			return null;
		}
		for (;;) {
			int time =  (int) (System.currentTimeMillis() - begin);
			if (time > timeout) {
				break;
			}
			int ret = waitForRead(timeout - time);
			 if (ret < 0) {
				break;
			}
			resp = recvResponse();
			if (resp != null) {
				break;
			}
		}
		return resp;
	}

	public int ready() {
		if (waitForRead() < 0) {
			return -1;
		}
		return getCode(recvResponse());
	}

	public int helo(String data) {
		return getCode(ask(CMD_HELO, data));
	}

	public int mailfrom(String data) {
		return getCode(ask(CMD_MAIL_FROM, data));
	}

	public int quit() {
		return getCode(ask(CMD_QUIT, null));
	}

	public static int checkMail(String mail, int port, String helo,
			String from, int timeout) {
		ZSmtp smtp = new ZSmtp();
		smtp.setTimeout(timeout);
		if (port < 0) {
			port = SMTP_PORT;
		}
		int ret = smtp.connect(mail, port);
		if (ret != 0) {
			return smtp.getError();
		}
		int code = 0;
		code = smtp.ready();
		if (code != CODE_READY) {
			return smtp.getError();
		}
		if (helo == null) {
			helo = ZDns.getPubIp(true);
		}
		smtp.helo(helo);
		if (from == null) {
			from = "info@mail.com";
		}
		smtp.mailfrom(from);
		code = ZSmtp.getCode(smtp.ask(ZSmtp.CMD_RCPT_TO, mail));
		smtp.quit();
		smtp.close();
		if (code < 0) {
			return smtp.getError();
		}
		return code;
	}
	
	public static int checkMail(String mail, int port, String helo,
			String from, int timeout, int retry) {
		int code = -1;
		// while (retry-- > 0) {
		for (int t = 0; t < retry; t++) {
			long begin = System.currentTimeMillis();
			code = checkMail(mail, port, helo, from, timeout);
			if (code > 0) {
				break;
			}
			int time = (int) (System.currentTimeMillis() - begin);
			if (time >= timeout) {
				continue;
			}
			try {
				Thread.sleep(timeout - time);
			} catch (InterruptedException e) {
			}
		}
		return code;
	}

	public static String strcode(int code) {
		String str = "UNKNOWN!";
		switch (code) {
		case ERR_MX:
			str = "MX NOT FOUND!";
			break;
		case ERR_CONN:
			str = "CONNECT FAIL!";
			break;
		case CODE_READY:
			str = "READY";
			break;
		case CODE_OK:
			str = "OK";
			break;
		case CODE_UNAVAILABLE:
			str = "UNAVAILABLE";
			break;
		}
		return str;
	}
}
